{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5172ee2e",
   "metadata": {},
   "source": [
    "# Numerical Python Primer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d24cf7d9",
   "metadata": {},
   "source": [
    "OpenPNM uses two very common python data structures, so it's best to get comfortable with them right away.  In this example we'll cover:\n",
    "\n",
    "Topics Covered\n",
    "- Python's ``list`` \n",
    "- Numpy's ``ndarray`` \n",
    "- Python's `dict` or *dictionary* \n",
    "- Subclassing a `dict` "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8ef6811",
   "metadata": {},
   "source": [
    "## Python Lists: Flexible but Slow\n",
    "First lets quickly look at Python's version of an *array*, which is called a ``list``.  It is indicated by square brackets:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "f11dc182",
   "metadata": {},
   "outputs": [],
   "source": [
    "L = [0, 2, 4, 6, 8]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4049cc31-21d3-4126-81a5-fcc02e734cb3",
   "metadata": {},
   "source": [
    "```{Note} \n",
    "Note that round brackets indicate a \"tuple\" which is basically the same as a list except they are **immutable**, meaning their values cannot be changed.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32b661dd",
   "metadata": {},
   "source": [
    "You can read and write values in a list as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "8044b257",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[32, 2, 4, 6, 8]\n"
     ]
    }
   ],
   "source": [
    "L[0] = L[2]*L[4]\n",
    "print(L)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1952522b",
   "metadata": {},
   "source": [
    "```{Note}\n",
    "Python uses 0-indexing, and also that square brackets are used to index into a sequence while round brackets are used in function calls.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6846474",
   "metadata": {},
   "source": [
    "You can make the ``list`` longer by ``append``ing a single item:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "ff50442c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[32, 2, 4, 6, 8, 100]\n"
     ]
    }
   ],
   "source": [
    "L.append(100)\n",
    "print(L)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb88fcf3",
   "metadata": {},
   "source": [
    "``extend``ing it with another list of items:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "31671492",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[32, 2, 4, 6, 8, 100, 200, 300]\n"
     ]
    }
   ],
   "source": [
    "L.extend([200, 300])\n",
    "print(L)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f0aac2d3",
   "metadata": {},
   "source": [
    "Or removing items:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "9562a42c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[32, 2, 6, 8, 100, 200, 300]\n"
     ]
    }
   ],
   "source": [
    "L.pop(2)\n",
    "print(L)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "128cabfc",
   "metadata": {},
   "source": [
    "However, the ``list`` is not very good at math:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "ea4c14a8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Adding to a list assumes you are joining 2 lists\n",
      "[32, 2, 6, 8, 100, 200, 300, 2, 3]\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    print(L + 2)\n",
    "except TypeError:\n",
    "    print('Adding to a list assumes you are joining 2 lists')\n",
    "print(L + [2, 3])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f83d880b",
   "metadata": {},
   "source": [
    "And multiplication assumes you want to duplicate the list N times:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "02de8b44",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[32, 2, 6, 8, 100, 200, 300, 32, 2, 6, 8, 100, 200, 300]\n"
     ]
    }
   ],
   "source": [
    "print(L*2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9ccec19",
   "metadata": {},
   "source": [
    "The reason the list is not ideal for numerical operations is that *anything* can be stored in each location:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "3c612173",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['str', 2, 6, 8, 100, 200, 300]\n"
     ]
    }
   ],
   "source": [
    "L[0] = 'str'\n",
    "print(L)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd7827f4",
   "metadata": {},
   "source": [
    "This is why it's not possible to add or multiply a list, since python has no way of knowing the meaning of adding an integer and a string (i.e. 'one' + 1.0).\n",
    "\n",
    "For a more detailed introduction to `list`s there are many resources on the internet, such as the [official docs](https://docs.python.org/3/tutorial/datastructures.html) and other tutorials like [this one](https://realpython.com/python-lists-tuples/)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a24d438",
   "metadata": {},
   "source": [
    "## Numpy ``ndarray``: Optimized for Numerics\n",
    "\n",
    "Given the above noted limitations of python for math, it is natural to wonder why use python for scientific work. The answer to this question is Numpy. Numpy has been around almost as long as python, and it is used almost exclusively in scientific python applications because as discussed above the native `list` is not very fast. Numpy arrays, or `ndarray`s, on the other hand are actually \"C\" arrays behind the scenes so are very fast. The downside is that you must learn a 'mini-language' to use them. The following few code blocks illustrate this.  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aef206a9",
   "metadata": {},
   "source": [
    "```{seealso}\n",
    "There are many resources for learning and understanding Numpy arrays, such as the [official numpy user guide](https://numpy.org/doc/stable/user/index.html#user) and this fun [illustrated guide](https://betterprogramming.pub/numpy-illustrated-the-visual-guide-to-numpy-3b1d4976de1d).\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "d113e3fc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ 0 15 30 45 60 75 90]\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "a = np.arange(0, 100, 15)\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dfa588e4",
   "metadata": {},
   "source": [
    "This is an example of the 'mini-language' that you need to learn, since ``arange`` is the Numpy version of ``range``.  The Numpy package has hundreds of functions available, and to be proficient with Numpy you need to at least be aware of most of them. You can see a list by typing ``dir(np)``.  "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bae52efe",
   "metadata": {},
   "source": [
    "Like the ``list`` you can index into Numpy arrays for reading and writing:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "a7fded9e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "30\n"
     ]
    }
   ],
   "source": [
    "print(a[2])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "47388a7d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[999  15  30  45  60  75  90]\n"
     ]
    }
   ],
   "source": [
    "a[0] = 999\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a8ff417",
   "metadata": {},
   "source": [
    "You can also use what is called 'fancy indexing', which allows you to index into an ``ndarray`` with another array:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "8630d254",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[999  30  60]\n"
     ]
    }
   ],
   "source": [
    "b = [0, 2, 4]\n",
    "print(a[b])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf21d771",
   "metadata": {},
   "source": [
    "You can set multiple locations with a single value:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "id": "7b03303e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[-100   15 -100   45 -100   75   90]\n"
     ]
    }
   ],
   "source": [
    "a[b] = -100\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c08b9aeb",
   "metadata": {},
   "source": [
    "Or an array of values:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "id": "7c0cb79f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[-100  111 -100  222 -100  333   90]\n"
     ]
    }
   ],
   "source": [
    "a[[1, 3, 5]] = [111, 222, 333]\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1c79ac1b",
   "metadata": {},
   "source": [
    "You can also use \"masks\" of boolean values, which is interpreted to mean any index where the mask is ``True``:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "262b3c26",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[  0 111   0 222   0 333  90]\n"
     ]
    }
   ],
   "source": [
    "mask = a < 0\n",
    "a[mask] = 0\n",
    "print(a)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "162b7cfd",
   "metadata": {},
   "source": [
    "### Vectorization\n",
    "\n",
    "And of course, math makes sense to an `ndarray` since *all* the elements are assured to be numbers.  This means that an `ndarray` can be multiplied by 2 or by another array (of the same length). This is called vectorization, since the array are operated on elementwise as you would do when adding two vectors. The alternative would be matrix operations, which are possible but are not the default behavior (unlike in Matlab where all operations are assumed to be matrix operations)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "b9007729",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[  0 222   0 444   0 666 180]\n"
     ]
    }
   ],
   "source": [
    "print(a*2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "501c3cd3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[100 211 100 322 100 433 190]\n"
     ]
    }
   ],
   "source": [
    "print(a + 100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "id": "80206e56",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[     0  12321      0  49284      0 110889   8100]\n"
     ]
    }
   ],
   "source": [
    "print(a*a)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fe97fab",
   "metadata": {},
   "source": [
    "### Methods versus Functions\n",
    "\n",
    "One of the tricks when first learning numpy is recognizing the difference (of lack thereof) between the methods attached to numpy objects, and the functions offered at the top level of the numpy package.  For instance, consider finding the sum of the following array:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "1bd2fc14",
   "metadata": {},
   "outputs": [],
   "source": [
    "arr = np.array([1, 2, 3, 4])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a248cb8",
   "metadata": {},
   "source": [
    "This can be done by passing `arr` to the numpy `sum` function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "id": "bf70d247",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10\n"
     ]
    }
   ],
   "source": [
    "print(np.sum(arr))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "635dd283",
   "metadata": {},
   "source": [
    "Or by calling the `sum` method that is attached to the `arr` object:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "e6792a64",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10\n"
     ]
    }
   ],
   "source": [
    "print(arr.sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0caea28",
   "metadata": {},
   "source": [
    "These are both equally valid and computationally identical behind the scenes.  The first option may be more familiar if you have used matlab.  The first option also offers a several more functions since some require two arrays as arguments.  The second option is really just for readability and for people who prefer to work with \"objects\"."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e684534",
   "metadata": {},
   "source": [
    "## Dictionaries: Holding Things Together\n",
    "The last piece of the puzzle is Python's built-in ``dict`` which is much like a list, in the sense that it can act as a container for any datatype, but items are addressed by name instead of index number. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "171f689b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'arr': array([  0, 111,   0, 222,   0, 333,  90]), 'list': ['str', 2, 6, 8, 100, 200, 300]}\n"
     ]
    }
   ],
   "source": [
    "d = dict()\n",
    "d['arr'] = a\n",
    "d['list'] = L\n",
    "print(d)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d59514d4",
   "metadata": {},
   "source": [
    "You can retrieve any element or `value` by name, which is called the `key`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "id": "97f811be",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[  0 111   0 222   0 333  90]\n"
     ]
    }
   ],
   "source": [
    "print(d['arr'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "43b0f3d3",
   "metadata": {},
   "source": [
    "`'arr'` and `[  0 111   0 222   0 333  90]` are called \"key-value\" pairs.  You can get a list of all keys defined on a dict using:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "id": "f57d97f8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "dict_keys(['arr', 'list'])"
      ]
     },
     "execution_count": 54,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d.keys()"
   ]
  },
  {
   "cell_type": "raw",
   "id": "6961653a",
   "metadata": {},
   "source": [
    "Or see all the values with:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "id": "e701de7d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "dict_values([array([  0, 111,   0, 222,   0, 333,  90]), ['str', 2, 6, 8, 100, 200, 300]])"
      ]
     },
     "execution_count": 55,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d.values()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3fedfd0f",
   "metadata": {},
   "source": [
    "And adding new items is easy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "id": "19cc45ee",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'arr': array([  0, 111,   0, 222,   0, 333,  90]), 'list': ['str', 2, 6, 8, 100, 200, 300], 'test': 1.0}\n"
     ]
    }
   ],
   "source": [
    "d['test'] = 1.0\n",
    "print(d)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "425951a5",
   "metadata": {},
   "source": [
    "Deleting values is also possible:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "id": "5dca62d4",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "dict_keys(['arr', 'list'])\n"
     ]
    }
   ],
   "source": [
    "del d['test']\n",
    "print(d.keys())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "39c0036e",
   "metadata": {},
   "source": [
    "or if you want to remove the value *and* catch it for use:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "id": "1cff1d65",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'list': ['str', 2, 6, 8, 100, 200, 300]}\n"
     ]
    }
   ],
   "source": [
    "arr = d.pop('arr')\n",
    "print(d)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "783a1c06",
   "metadata": {},
   "source": [
    ":::{seealso}\n",
    "For a more detailed overview of using `dict`s, there are many internet tutorials such as [this one](https://python.land/python-data-types/dictionaries), [this one](https://towardsdatascience.com/a-complete-guide-to-dictionaries-in-python-5c3f4c132569), or [this one](https://realpython.com/python-dicts/).\n",
    ":::"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b44cc893",
   "metadata": {},
   "source": [
    "### Subclassing ``dict`` \n",
    "\n",
    "This is may seem like an intimidating concept at first, but it's actually beautifully simple once you get a feel for how it works. It's also relevant to learning OpenPNM since it uses ``dict``s extensively, and these are often subclassed to augment their functionality.  Subclassing means \"taking the basic functionality of the ``dict``, then adding to and/or changing it\". For a deep dive into the process of subclassing a `dict` refer to [this tutorial](https://realpython.com/inherit-python-dict/), but here we'll give a more basic introduction.\n",
    "\n",
    "To illustrate the idea of subclasses, as it pertains to OpenPNM, let's change how the reading and writing of items works. Whenever you use the square brackets ``[ ]`` to index into a ``dict``, this *actually* calls the ``__getitem__`` and ``__setitem__`` methods.  The double underscores indicate that these are intrinsic Python methods which the user should not call directly (sometimes called [magic methods](https://towardsdatascience.com/magic-methods-in-python-by-example-16b6826cae5c)), but they do the bulk of the work.  So let's try it out by creating a class that tells us what is going on each time we read and write something:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "id": "4cf90058",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Create a new class which is a dict PLUS the extra functionality we will add\n",
    "class NewDict(dict):\n",
    "\n",
    "    def __setitem__(self, key, value):\n",
    "        # This is our customization\n",
    "        print(\"The key being written is:\", key)\n",
    "        print(\"The value being written is:\", value)\n",
    "        # Now we call the setitem on the actual dict class\n",
    "        super().__setitem__(key, value)\n",
    "\n",
    "    def __getitem__(self, key):\n",
    "        print(\"The key being retrieved is:\", key)\n",
    "        return super().__getitem__(key)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "id": "306be022",
   "metadata": {},
   "outputs": [],
   "source": [
    "dnew = NewDict()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a97c5a8",
   "metadata": {},
   "source": [
    "The following will trigger the ``__setitem__`` method since we are \"setting\" the value of 1.0:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "id": "3af4dc99",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The key being written is: test\n",
      "The value being written is: 1.0\n"
     ]
    }
   ],
   "source": [
    "dnew['test'] = 1.0"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "537492c4",
   "metadata": {},
   "source": [
    "The following will trigger the `__getitem__` method since we are \"getting\" the value stored in `'test'`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "id": "99832f5b",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The key being retrieved is: test\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "1.0"
      ]
     },
     "execution_count": 62,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dnew['test']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a7adfcc",
   "metadata": {},
   "source": [
    "In OpenPNM, both the `__setitem__` and `__getitem__` methods are overloaded so that several checks are performed.  For instance, we enforce that all dictionary keys start with either `'pore'` or `'throat'`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "id": "ef7ec8dd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "All keys must start with either pore or throat\n"
     ]
    }
   ],
   "source": [
    "import openpnm as op\n",
    "pn = op.network.Demo()\n",
    "try:\n",
    "    pn['foo.bar'] = 1.0\n",
    "except Exception as e:\n",
    "    print(e)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "99172243",
   "metadata": {},
   "source": [
    "In the above cell block the statement within the ``try`` block raises and exception, which is caught in the ``except`` block, and the error message is printed indicating clearly what the problem is.\n",
    "\n",
    "We can implement this behavior in our ``NewDict`` class above as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "id": "529de5f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "class NewDict(dict):\n",
    "\n",
    "    def __setitem__(self, key, value):\n",
    "        if not (key.startswith('pore') or key.startswith('throat')):\n",
    "            raise Exception('Key must start with either pore, or throat')\n",
    "        super().__setitem__(key, value)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "id": "c0ae22fe",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Key must start with either pore, or throat\n",
      "dict_keys(['pore.test'])\n"
     ]
    }
   ],
   "source": [
    "dnew = NewDict()\n",
    "try:\n",
    "    dnew['foo.test'] = 1.0\n",
    "except Exception as e:\n",
    "    print(e)\n",
    "dnew['pore.test'] = 1.0\n",
    "print(dnew.keys())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65eaf894",
   "metadata": {},
   "source": [
    "Another useful feature of subclassing `dict`s is the ability to add *new* functions, not just overwriting the old ones. Let's add a method that prints all the pore properties, but not the throat properties:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "id": "234ea166",
   "metadata": {},
   "outputs": [],
   "source": [
    "class NewDict(dict):\n",
    "\n",
    "    def __setitem__(self, key, value):\n",
    "        if not (key.startswith('pore') or key.startswith('throat')):\n",
    "            raise Exception('Key must start with either pore, or throat')\n",
    "        super().__setitem__(key, value)\n",
    "\n",
    "    def poreprops(self):\n",
    "        print('The following pore properties were found:')\n",
    "        for item in self.keys():\n",
    "            if item.startswith('pore'):\n",
    "                print('-> ' + item)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "id": "becd2ef0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The following pore properties were found:\n",
      "-> pore.test\n",
      "-> pore.bar\n"
     ]
    }
   ],
   "source": [
    "dnew = NewDict()\n",
    "dnew['pore.test'] = 1.0\n",
    "dnew['pore.bar'] = 100\n",
    "dnew['throat.blah'] = 2.0\n",
    "dnew.poreprops()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f275fa33",
   "metadata": {},
   "source": [
    "And finally, you can add attributes to a subclassed `dict`, such as a name:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "id": "8c1d1758",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "bob\n"
     ]
    }
   ],
   "source": [
    "dnew.name = 'bob'\n",
    "print(dnew.name)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7cf58a86",
   "metadata": {},
   "source": [
    "You can also add other dictionaries!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "id": "da5cabea",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{'name': 'bob'}\n"
     ]
    }
   ],
   "source": [
    "dnew.settings = {}\n",
    "dnew.settings['name'] = 'bob'\n",
    "print(dnew.settings)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9352d3c",
   "metadata": {},
   "source": [
    "So in summary, `dict`s are extremely versatile and handy \"data containers\".  You can alter their built-in behavior to do things like enforcing that all data are numpy arrays; you can add new methods the allow users interact with the data in specific ways; and you can add data attributes to help users get quick access to needed information.  "
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
